﻿// Most of this code is from http://jasonjano.wordpress.com/2010/02/09/a-simple-c-wrapper-for-ffmpeg/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;

using System.Diagnostics;
using System.ComponentModel;
using System.IO;
using System.Text.RegularExpressions;

namespace mediaEncoder
{
    /// <summary>
    /// Info used to convert the media. Most fields can be left blank for their defualts.
    /// </summary>
    public struct EncodeInfo
    {
        /// <summary>
        /// Full path to the media to be encoded.
        /// </summary>
        public String MediaPath;
        /// <summary>
        /// The codec to be converted to.
        /// </summary>
        public String Codec;
        /// <summary>
        /// The Max Bitrate.
        /// </summary>
        public String BitRateMax;
        /// <summary>
        /// Filename of the files to be created.
        /// </summary>
        public String OutputFileName;
        /// <summary>
        /// Full path to the folder where the file should be created
        /// </summary>
        public String OutPutFolder;
        /// <summary>
        /// The type of media your trying to created. E.G. dvd, vcd, svcd, bluray. Add "-pal" or "-ntsc" to use those standards. This variable can be used to set the default encoding options for the specified target.
        /// </summary>
        public String Target;
    }

    public struct OutputPackage
    {
        public String MediaFile { get; set; }
        public System.Drawing.Image PreviewImage { get; set; }
        public string RawOutput { get; set; }
        public bool Success { get; set; }
    }

    public class MediaEncoder
    {
        //OutputPackage ou;

        public delegate void LogHandler(String Message);
        public delegate void FFMpegUpdateHandler(String Message);

        public event LogHandler Log;
        public event FFMpegUpdateHandler FFMpegUpdate;

        #region varibles

        Process proc = null;

        private bool stopping = false;

        private string _ffExe;

        private string _WorkingPath;
        public string WorkingPath
        {
            get
            {
                return _WorkingPath;
            }
            set
            {
                _WorkingPath = value;
            }
        }

        #endregion

        public MediaEncoder(String ffMpegloc, String WorkingFolder)
        {
            _ffExe = ffMpegloc;
            _WorkingPath = WorkingFolder;

        }

        //Make sure we have valid ffMpeg.exe file and working directory to do our dirty work.
        public bool Initialize()
        {
            OnLog("FFMpeg location is: " + _ffExe);
            OnLog("Working Path is: " + _WorkingPath);

	        //first make sure we have a value for the ffexe file setting
	        if (string.IsNullOrEmpty(_ffExe))
	        {
                OnLog("FFMpeg is missing");
                return false;
	        }

	        //Now see if ffmpeg.exe exists
	        string workingpath = GetWorkingFile();
	        if (string.IsNullOrEmpty(workingpath))
	        {
		        //ffmpeg doesn't exist at the location stated.
		        OnLog("Could not find a copy of ffmpeg.exe");
	        }
	        _ffExe = workingpath;

	        //now see if we have a temporary place to work
	        if (string.IsNullOrEmpty(_WorkingPath))
	        {
                return false;
	        }

            return true;
        }

        private string GetWorkingFile()
        {
	        //try the stated directory
	        if (File.Exists(_ffExe))
	        {
		        return _ffExe;
	        }

	        //oops, that didn't work, try the base directory
	        if (File.Exists(Path.GetFileName(_ffExe)))
	        {
		        return Path.GetFileName(_ffExe);
	        }

	        //well, now we are really unlucky, let's just return null
	        return null;
        }

        public static System.Drawing.Image LoadImageFromFile(string fileName)
        {
	        System.Drawing.Image theImage = null;
	        using (FileStream fileStream = new FileStream(fileName, FileMode.Open,
	        FileAccess.Read))
	        {
		        byte[] img;
		        img = new byte[fileStream.Length];
		        fileStream.Read(img, 0, img.Length);
		        fileStream.Close();
		        theImage = System.Drawing.Image.FromStream(new MemoryStream(img));
		        img = null;
	        }
	        GC.Collect();
	        return theImage;
        }

        public static MemoryStream LoadMemoryStreamFromFile(string fileName)
        {
	        MemoryStream ms = null;
	        using (FileStream fileStream = new FileStream(fileName, FileMode.Open,
	        FileAccess.Read))
	        {
		        byte[] fil;
		        fil = new byte[fileStream.Length];
		        fileStream.Read(fil, 0, fil.Length);
		        fileStream.Close();
		        ms = new MemoryStream(fil);
	        }
	        GC.Collect();
	        return ms;
        }

        //Note the private call here and the argument for Parameters.  The private call is
        //being made here because, in this class, we don't really want to have this method
        //called from outside of the class -- this, however flies in the face of allowing the
        //parameters argument (why not just allow out the public call so that a developer can
        //put in the parameters from their own code?  I guess one could do it and it would probably
        //work fine but, for this implementation, I chose to leave it private.
        private String RunProcess(string Parameters)
        {
            OnLog("Running FFMpeg at " + _ffExe + " with parameters: " + Parameters);

            //create a process info object so we can run our app
            ProcessStartInfo oInfo = new ProcessStartInfo(this._ffExe, Parameters);
            oInfo.UseShellExecute = false;
            oInfo.CreateNoWindow = true;

            //so we are going to redirect the output and error so that we can parse the return
            oInfo.RedirectStandardOutput = true;
            oInfo.RedirectStandardError = true;

            //Create the output and streamreader to get the output
            string output = null;
            StreamReader srOutput = null;

            
            proc = new System.Diagnostics.Process();
            proc.StartInfo = oInfo;


            //try the process
            try
            {
                //run the process


                //proc.WaitForExit();
                if (!proc.Start())
                {
                    OnLog("Error starting FFMpeg");
                }

                proc.PriorityClass = ProcessPriorityClass.BelowNormal;

                //get the output
                srOutput = proc.StandardError;
                

                String line;
                while ((line = srOutput.ReadLine()) != null)
                {
                    if (stopping)
                    {
                        OnLog("Media Encoder stopping");
                        break;
                    }
                    //OnUpdate(line);
                    output += line;
                    OnUpdate(line);
                }
                if (!stopping)
                {

                    //now put it in a string
                    output = srOutput.ReadToEnd();

                }
            }
            catch (Exception ex)
            {
                OnLog(ex.ToString());
                output = string.Empty;
            }
            finally
            {
                if (!proc.HasExited)
                {
                    OnLog("Killing FFMPEG");
                    proc.Kill();
                }
                proc.Close();

                stopping = false;

                //now, if we succeded, close out the streamreader
                if (srOutput != null)
                {
                    srOutput.Close();
                    srOutput.Dispose();
                }
            }
            OnLog("Encoding process finished.");

            return output;
        }

        //We are going to take in memory stream for this file to allow for different input options.
        //Unfortunately, this means that we also need the file extension to pass it out to ffMpeg.
        public MediaFile GetVideoInfo(MemoryStream inputFile, string Filename)
        {
            //Create a temporary file for our use in ffMpeg
            string tempfile = Path.Combine(this.WorkingPath, System.Guid.NewGuid().ToString() + Path.GetExtension(Filename));
            FileStream fs = File.Create(tempfile);

            //write the memory stream to a file and close our the stream so it can be used again.
            inputFile.WriteTo(fs);
            fs.Flush();
            fs.Close();
            GC.Collect();

            //Video File has some basic information about the video
            MediaFile vf = null;
            try
            {
                vf = new MediaFile(tempfile);
            }
            catch (Exception ex)
            {
                OnLog(ex.ToString());
            }

            //And, without adieu, a call to our main method for this functionality.
            GetVideoInfo(vf);

            try
            {
                File.Delete(tempfile);
            }
            catch (Exception)
            {

            }

            return vf;
        }

        //This sub is just another overload to allow input of a direct path, we are just
        //using the videofile class to send in.  More on the video file class further down
        //the article.
        public MediaFile GetVideoInfo(string inputPath)
        {
            MediaFile vf = null;
            try
            {
                vf = new MediaFile(inputPath);
            }
            catch (Exception ex)
            {
                OnLog(ex.ToString());
            }
            GetVideoInfo(vf);
            return vf;
        }

        //And now the important code for the GetVideoInfo
        public void GetVideoInfo(MediaFile input)
        {
            //set up the parameters for video info -- these will be passed into ffMpeg.exe
            string Params = string.Format("-i {0}", input.Path);
            string output = ""; //RunProcess(Params);
            input.RawInfo = output;

            //Use a regular expression to get the different properties from the video parsed out.
            Regex re = new Regex("[D|d]uration:.((\\d|:|\\.)*)");
            Match m = re.Match(input.RawInfo);

            if (m.Success)
            {
                string duration = m.Groups[1].Value;
                string[] timepieces = duration.Split(new char[] { ':', '.' });
                if (timepieces.Length == 4)
                {
                    input.Duration = new TimeSpan(0, Convert.ToInt16(timepieces[0]), Convert.ToInt16(timepieces[1]), Convert.ToInt16(timepieces[2]), Convert.ToInt16(timepieces[3]));
                }
            }

            //get audio bit rate
            re = new Regex("[B|b]itrate:.((\\d|:)*)");
            m = re.Match(input.RawInfo);
            double kb = 0.0;
            if (m.Success)
            {
                Double.TryParse(m.Groups[1].Value, out kb);
            }
            input.BitRate = kb;

            //get the audio format
            re = new Regex("[A|a]udio:.*");
            m = re.Match(input.RawInfo);
            if (m.Success)
            {
                input.AudioFormat = m.Value;
            }

            //get the video format
            re = new Regex("[V|v]ideo:.*");
            m = re.Match(input.RawInfo);
            if (m.Success)
            {
                input.VideoFormat = m.Value;
            }

            //get the video format
            re = new Regex("(\\d{2,3})x(\\d{2,3})");
            m = re.Match(input.RawInfo);
            if (m.Success)
            {
                int width = 0; int height = 0;
                int.TryParse(m.Groups[1].Value, out width);
                int.TryParse(m.Groups[2].Value, out height);
                input.Width = width;
                input.Height = height;
            }
            input.infoGathered = true;
        }


        #region Convert Funcions

        public OutputPackage ConvertMedia(EncodeInfo Info)
        {
            String finalpath;
            String filename;
            String Params = "";

            OutputPackage ou = new OutputPackage();

            if (String.IsNullOrEmpty(Info.MediaPath))
            {
                OnLog("Encode Failed: No input file given.");
                ou.Success = false;
                return ou;
            }

            try
            {
                MediaFile inputfile = new MediaFile(Info.MediaPath);
                GetVideoInfo(inputfile);

                #region Check For Missing Encode Info

                if (String.IsNullOrEmpty(Info.OutputFileName))
                {
                    filename = System.Guid.NewGuid().ToString() + "." + Info.Codec;
                }
                else
                    filename = Info.OutputFileName;

                if (String.IsNullOrEmpty(Info.OutPutFolder))
                {
                    finalpath = Path.Combine(this.WorkingPath, Info.OutputFileName);
                }
                else
                    finalpath = Path.Combine(Info.OutPutFolder, Info.OutputFileName);

                #endregion

                #region Build params string

                Params += " -i \"" + inputfile.Path + "\"";

                if (!String.IsNullOrEmpty(Info.Codec))
                    Params += " -f " + Info.Codec;

                if (!String.IsNullOrEmpty(Info.Target))
                    Params += " -target " + Info.Target;

                Params += " -y";

                // this should always be last
                Params += " \"" + finalpath + "\"";

                #endregion
            }
            catch (Exception ex)
            {
                OnLog("Error setting up file " + Info.MediaPath + " for encode. Error was: " + ex.ToString());
            }

            


  
                        
            RunProcess(Params);

            

            //Check to see if our conversion file exists and then load the converted
            //file into our output package.
            if (File.Exists(ou.MediaFile))
                ou.Success = true;

            return ou;
        }

        //The actually important code, rather than an overload.
        /*public OutputPackage ConvertToFLV(MediaFile input)
        {
            //check to see if we have already gathered our info so we know
            //where to get our preview image from.
            if (!input.infoGathered)
            {
                GetVideoInfo(input);
            }

            //Create our output object
            OutputPackage ou = new OutputPackage();

            //set up the parameters for getting a previewimage
            string filename = System.Guid.NewGuid().ToString() + ".jpg";
            int secs;

            //divide the duration in 3 to get a preview image in the middle of the clip
            //instead of a black image from the beginning.
            secs = (int)Math.Round(TimeSpan.FromTicks(input.Duration.Ticks / 3).TotalSeconds, 0);

            string finalpath = Path.Combine(this.WorkingPath, filename);

            //These are the parameters for setting up a preview image that must be passed to ffmpeg.
            //Note that we are asking for a jpeg image at our specified seconds.
            string Params = string.Format("-i {0} {1} -vcodec mjpeg -ss {2} -vframes 1 -an -f rawvideo", input.Path, finalpath, secs);
            string output = RunProcess(Params);

            ou.RawOutput = output;

            //Ok, so hopefully we now have a preview file.  If the file
            //did not get created properly, try again at the first frame.
            if (File.Exists(finalpath))
            {
                //load that file into our output package and attempt to delete the file
                //since we no longer need it.
                ou.PreviewImage = LoadImageFromFile(finalpath);
                try
                {
                    File.Delete(finalpath);
                }
                catch (Exception) { }
            }
            else
            { //try running again at frame 1 to get something
                Params = string.Format("-i {0} {1} -vcodec mjpeg -ss {2} -vframes 1 -an -f rawvideo", input.Path, finalpath, 1);
                output = RunProcess(Params);

                ou.RawOutput = output;

                if (File.Exists(finalpath))
                {
                    ou.PreviewImage = LoadImageFromFile(finalpath);
                    try
                    {
                        File.Delete(finalpath);
                    }
                    catch (Exception) { }
                }
            }

            finalpath = Path.Combine(this.WorkingPath, filename);
            filename = System.Guid.NewGuid().ToString() + ".flv";

            //Now we are going to actually create the converted file.  Note that we are asking for
            //a video at 22khz 64bit.  This can be changed by a couple quick alterations to this line,
            //or by extending out this class to offer multiple different conversions.
            Params = string.Format("-i {0} -y -ar 22050 -ab 64 -f flv {1}", input.Path, finalpath);
            output = RunProcess(Params);

            //Check to see if our conversion file exists and then load the converted
            //file into our output package.  If the file does exist and we are able to
            //load it into our output package, we can delete the work file.
            if (File.Exists(finalpath))
                ou.MediaFile = finalpath;
            
            return ou;
        }*/
        #endregion

        protected void OnLog(String Message)
        {
            if (Log != null)
                Log(Message);
        }

        protected void OnUpdate(String Message)
        {
            if (FFMpegUpdate != null)
                FFMpegUpdate(Message);
        }

        public void Stop()
        {
            stopping = true;
        }
    }
}
